# Copyright 2013 The Distro Tracker Developers
# See the COPYRIGHT file at the top-level directory of this distribution and
# at http://deb.li/DTAuthors
#
# This file is part of Distro Tracker. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution and at http://deb.li/DTLicense. No part of Distro Tracker,
# including this file, may be copied, modified, propagated, or distributed
# except according to the terms contained in the LICENSE file.
"""
Tests for the :mod:`distro_tracker.auto_news` app.
"""
from __future__ import unicode_literals
from distro_tracker.test import TestCase
from django.core.files.base import ContentFile
from django.utils.six.moves import mock
from distro_tracker.core.models import ExtractedSourceFile
from distro_tracker.core.models import News
from distro_tracker.core.models import SourcePackageName, SourcePackage
from distro_tracker.core.models import SourcePackageRepositoryEntry
from distro_tracker.core.models import Repository
from distro_tracker.core.tasks import Job
from distro_tracker.core.tasks import JobState
from distro_tracker.core.tasks import Event
from distro_tracker.auto_news.tracker_tasks \
    import GenerateNewsFromRepositoryUpdates


class GenerateNewsFromRepositoryUpdatesTest(TestCase):
    """
    Tests the news generated by various repository updates.
    """
    def setUp(self):
        self.job_state = mock.create_autospec(JobState)
        self.job_state.events_for_task.return_value = []
        self.job = mock.create_autospec(Job)
        self.job.job_state = self.job_state
        self.generate_news_task = GenerateNewsFromRepositoryUpdates()
        self.generate_news_task.job = self.job

    def add_mock_events(self, name, arguments):
        """
        Helper method adding mock events which the news generation task will
        see when it runs.
        """
        self.job_state.events_for_task.return_value.append(
            Event(name=name, arguments=arguments)
        )

    def run_task(self):
        self.generate_news_task.execute()

    def create_source_package(self, name, version, events=True):
        """
        Helper method which creates a new source package and makes sure all
        events that would have been raised on creating the package are
        passed to the news generation task if the ``events`` flag is set.

        :param name: The name of the source package to create
        :param version: The version of the source package which is created
        :param events: A flag indicating whether the corresponding events
            should be passed to the generation task when it runs.
        """
        # Make sure the source package name object exists
        src_pkg_name, _ = SourcePackageName.objects.get_or_create(name=name)
        src_pkg, _ = SourcePackage.objects.get_or_create(
            source_package_name=src_pkg_name, version=version)
        # Add all events for a newly created source package which the task will
        # receive.
        if events:
            self.add_mock_events('new-source-package-version', {
                'name': name,
                'version': version,
            })

        return src_pkg

    def add_source_package_to_repository(self, name, version, repository,
                                         events=True):
        """
        Helper method which adds a source package to the given repository
        and makes sure the corresponding events are received by the
        news generation task if the ``events`` flag is set.

        :param name: The name of the source package
        :param version: The version of the source package
        :param repository: The repository to which to add the source package
        :param events: A flag indicating whether the corresponding events
            should be passed to the generation task when it runs.
        """
        repo, _ = Repository.objects.get_or_create(name=repository, defaults={
            'shorthand': repository[:10],
            'suite': 'suite',
            'components': ['component']
        })

        source_package = SourcePackage.objects.get(
            source_package_name__name=name,
            version=version)

        entry = SourcePackageRepositoryEntry(
            repository=repo,
            source_package=source_package)
        entry.save()

        if events:
            self.add_mock_events('new-source-package-version-in-repository', {
                'name': name,
                'version': version,
                'repository': repository,
            })

    def remove_source_package_from_repository(self, name, version, repository,
                                              events=True):
        """
        Helper method which removes the given source package version from the
        given repository. It makes sure the corresponding events are received
        by the news generation task if the ``events`` flag is set.
        """
        SourcePackageRepositoryEntry.objects.filter(
            source_package__source_package_name__name=name,
            source_package__version=version,
            repository__name=repository).delete()
        if events:
            self.add_mock_events('lost-source-package-version-in-repository', {
                'name': name,
                'version': version,
                'repository': repository,
            })

    def assert_correct_accepted_message(self, title,
                                        package_name, version, repository):
        self.assertEqual(
            '{pkg} {ver} has been added to {repo}'.format(
                pkg=package_name, ver=version, repo=repository),
            title
        )

    def assert_correct_migrated_message(self, title,
                                        package_name, version, repository):
        self.assertEqual(
            '{pkg} {ver} migrated to {repo}'.format(
                pkg=package_name, ver=version, repo=repository),
            title
        )

    def assert_correct_removed_message(self, title, package_name, repository):
        self.assertEqual(
            '{pkg} has been removed from {repo}'.format(
                pkg=package_name, repo=repository),
            title
        )

    def test_new_source_package(self):
        """
        Tests the case when a completely new source package is created (it was
        not seen in any repository previously).
        """
        source_package_name = 'dummy-package'
        source_package_version = '1.0.0'
        repository_name = 'some-repository'
        self.create_source_package(source_package_name, source_package_version)
        self.add_source_package_to_repository(
            source_package_name, source_package_version, repository_name)

        self.run_task()

        # A news item was created
        self.assertEqual(1, News.objects.count())
        news = News.objects.all()[0]
        self.assertEqual(news.package.name, source_package_name)
        self.assert_correct_accepted_message(
            news.title,
            source_package_name, source_package_version, repository_name)

    def test_new_source_package_in_hidden_repository(self):
        """
        Tests the case when a completely new source package is created and
        appears in a hidden repository.
        """
        source_package_name = 'dummy-package'
        source_package_version = '1.0.0'
        repository_name = 'some-repository'
        self.create_source_package(source_package_name, source_package_version)
        self.add_source_package_to_repository(
            source_package_name, source_package_version, repository_name)
        Repository.objects.get(name=repository_name).flags.create(name='hidden',
                                                                  value=True)

        self.run_task()

        # No news item is created
        self.assertEqual(0, News.objects.count())

    def test_new_source_package_version(self):
        """
        Tests the case when a new version of an already existing source package
        is created.
        """
        source_package_name = 'dummy-package'
        source_package_version = '1.1.0'
        repository_name = 'some-repository'
        # Create the package, but do not add those events to the task
        self.create_source_package(
            source_package_name, source_package_version, events=False)
        # Add the package to the repository
        self.add_source_package_to_repository(
            source_package_name, source_package_version, repository_name)

        self.run_task()

        # A news item was created
        self.assertEqual(1, News.objects.count())
        news = News.objects.all()[0]
        self.assertEqual(news.package.name, source_package_name)
        self.assert_correct_migrated_message(
            news.title,
            source_package_name, source_package_version, repository_name)

    def test_new_source_package_version_in_hidden_repository(self):
        """
        Tests the case when a new version of an already existing source package
        is created in a hidden repository.
        """
        source_package_name = 'dummy-package'
        source_package_version = '1.1.0'
        repository_name = 'some-repository'
        # Create the package, but do not add those events to the task
        self.create_source_package(
            source_package_name, source_package_version, events=False)
        # Add the package to the repository
        self.add_source_package_to_repository(
            source_package_name, source_package_version, repository_name)
        Repository.objects.get(name=repository_name).flags.create(name='hidden',
                                                                  value=True)

        self.run_task()

        # No news item is created
        self.assertEqual(0, News.objects.count())

    def test_new_source_package_version_replaces_old_one(self):
        """
        Tests the case when a new version of an already existing source
        package is created and added to the repository which contains
        the old package version.
        """
        source_package_name = 'dummy-package'
        old_version = '1.0.0'
        new_version = '1.1.0'
        repository = 'repo'
        # Create the old version and make sure it is already in the
        # repository
        self.create_source_package(
            source_package_name, old_version, events=False)
        self.add_source_package_to_repository(
            source_package_name, old_version, repository, events=False)
        # Now create the new version and make it replace the old version
        # in the repository
        self.create_source_package(source_package_name, new_version)
        self.add_source_package_to_repository(
            source_package_name, new_version, repository)
        self.remove_source_package_from_repository(
            source_package_name, old_version, repository)

        self.run_task()

        # Only one news item is created
        self.assertEqual(1, News.objects.count())

    def test_multiple_new_versions_same_repo(self):
        """
        Tests the case when there are multiple new versions in a repository.
        """
        source_package_name = 'dummy-package'
        versions = ['1.0.0', '1.1.0']
        repository_name = 'some-repository'
        # Create the package versions
        for version in versions:
            self.create_source_package(source_package_name, version)
            self.add_source_package_to_repository(
                source_package_name, version, repository_name)

        self.run_task()

        # Two news items exist
        self.assertEqual(2, News.objects.count())
        titles = [news.title for news in News.objects.all()]
        # This is actually a sort by version found in the title
        titles.sort()
        for title, version in zip(titles, versions):
            self.assert_correct_accepted_message(
                title,
                source_package_name, version, repository_name
            )

    def test_multiple_new_versions_different_repos(self):
        """
        Tests the case when there are mutliple new versions of a source package
        each in a different repository.
        """
        source_package_name = 'dummy-package'
        versions = ['1.0.0', '1.1.0']
        repositories = ['repo1', 'repo2']
        # Create these versions
        for version, repository in zip(versions, repositories):
            self.create_source_package(source_package_name, version)
            self.add_source_package_to_repository(
                source_package_name, version, repository)

        self.run_task()

        self.assertEqual(2, News.objects.count())
        titles = [news.title for news in News.objects.all()]
        # This is actually a sort by version found in the title
        titles.sort()
        for title, version, repository_name in zip(titles, versions,
                                                   repositories):
            self.assert_correct_accepted_message(
                title,
                source_package_name, version, repository_name
            )

    def test_package_version_add_different_repos(self):
        """
        Tests the case where a single existing package version is added to two
        repositories.
        """
        source_package_name = 'dummy-package'
        version = '1.1.0'
        repositories = ['repo1', 'repo2']
        self.create_source_package(source_package_name, version, events=False)
        for repository in repositories:
            self.add_source_package_to_repository(
                source_package_name, version, repository)

        self.run_task()

        self.assertEqual(2, News.objects.count())
        # This is a sort by repository name
        titles = [news.title for news in News.objects.order_by('title')]
        for title, repository_name in zip(titles, repositories):
            self.assert_correct_migrated_message(
                title,
                source_package_name, version, repository_name
            )

    def test_package_version_add_different_repos_one_hidden(self):
        """
        Tests the case where a single existing package version is added to two
        repositories (one of them is hidden) and tests that a news is generated
        if the flag 'hidden' is false.
        """
        source_package_name = 'dummy-package'
        version = '1.1.0'
        repositories = ['repo1', 'repo2']
        self.create_source_package(source_package_name, version, events=False)
        for repository in repositories:
            self.add_source_package_to_repository(
                source_package_name, version, repository)
        Repository.objects.get(name='repo1').flags.create(name='hidden',
                                                          value=True)
        Repository.objects.get(name='repo2').flags.create(name='hidden',
                                                          value=False)

        self.run_task()

        self.assertEqual(1, News.objects.count())

    def test_package_version_updates_different_repos(self):
        """
        Tests the case where a single existing package version is added to two
        repositories replacing the versions previously found in those
        repositories.
        """
        source_package_name = 'dummy-package'
        old_version = '1.0.0'
        version = '1.1.0'
        repositories = ['repo1', 'repo2']
        self.create_source_package(source_package_name, version, events=False)
        self.create_source_package(
            source_package_name, old_version, events=False)
        for repository in repositories:
            # Old version
            self.add_source_package_to_repository(
                source_package_name, old_version, repository, events=False)
            # Replace the old version with the new one
            self.remove_source_package_from_repository(
                source_package_name, old_version, repository)
            self.add_source_package_to_repository(
                source_package_name, version, repository)

        self.run_task()

        # Only two news messages.
        self.assertEqual(2, News.objects.count())
        # This is a sort by repository name
        titles = [news.title for news in News.objects.order_by('title')]
        for title, repository_name in zip(titles, repositories):
            self.assert_correct_migrated_message(
                title,
                source_package_name, version, repository_name
            )

    def test_multiple_package_updates_different_repos(self):
        """
        Tests the case where different repositories get different new package
        versions when they already previously had another version of the
        package.
        """
        source_package_name = 'dummy-package'
        versions = ['1.1.0', '1.2.0']
        old_version = '1.0.0'
        repositories = ['repo1', 'repo2']
        for repository in repositories:
            self.create_source_package(
                source_package_name, old_version, events=False)
            self.add_source_package_to_repository(
                source_package_name, old_version, repository, events=False)
        # Add the new package version to each repository
        for version, repository in zip(versions, repositories):
            self.create_source_package(source_package_name, version)
            self.add_source_package_to_repository(
                source_package_name, version, repository)

        self.run_task()

        self.assertEqual(2, News.objects.count())
        titles = [news.title for news in News.objects.order_by('title')]
        titles.sort()
        for title, version, repository in zip(titles, versions, repositories):
            self.assert_correct_accepted_message(
                title,
                source_package_name, version, repository)

    def test_source_package_removed(self):
        """
        Tests the case where a single source package version is removed
        from a repository.
        """
        source_package_name = 'dummy-package'
        version = '1.0.0'
        repository = 'repo'
        self.create_source_package(source_package_name, version, events=False)
        self.add_source_package_to_repository(
            source_package_name, version, repository, events=False)
        self.remove_source_package_from_repository(
            source_package_name, version, repository)

        self.run_task()

        # A news item is created.
        self.assertEqual(1, News.objects.count())
        self.assert_correct_removed_message(
            News.objects.all()[0].title,
            source_package_name, repository
        )

    def test_source_package_removed_from_hidden_repository(self):
        """
        Tests the case where a single source package version is removed
        from a hidden repository.
        """
        source_package_name = 'dummy-package'
        version = '1.0.0'
        repository = 'repo'
        self.create_source_package(source_package_name, version, events=False)
        self.add_source_package_to_repository(
            source_package_name, version, repository, events=False)
        self.remove_source_package_from_repository(
            source_package_name, version, repository)
        Repository.objects.get(name=repository).flags.create(name='hidden',
                                                             value=True)

        self.run_task()

        # No news item is created.
        self.assertEqual(0, News.objects.count())

    def test_migrate_from_a_hidden_repository(self):
        """
        Tests the case where a package version existing in a hidden repository
        is added to one repository.
        """
        source_package_name = 'dummy-package'
        version = '1.0.0'
        repositories = ['repo1', 'repo2']

        self.create_source_package(
            source_package_name, version, events=False)
        self.add_source_package_to_repository(
            source_package_name, version, repositories[0], events=False)
        # Add the version to one repository
        self.add_source_package_to_repository(
            source_package_name, version, repositories[1])
        Repository.objects.get(name='repo1').flags.create(name='hidden',
                                                          value=True)

        self.run_task()

        # One news item - migrated to a non-hidden repository
        self.assertEqual(1, News.objects.count())
        news = News.objects.first()
        self.assert_correct_migrated_message(
            news.title, source_package_name, version, repositories[1])

    def test_multiple_versions_removed_same_repo(self):
        """
        Tests the case where multiple versions of the same package are removed
        from the same repository and there are no more remaining versions of
        the package in that repository.
        """
        source_package_name = 'dummy-package'
        versions = ['1.0.0', '1.1.0']
        repository = 'repo'
        for version in versions:
            self.create_source_package(
                source_package_name, version, events=False)
            self.add_source_package_to_repository(
                source_package_name, version, repository, events=False)
            self.remove_source_package_from_repository(
                source_package_name, version, repository)

        self.run_task()

        # Only one news item should be created
        self.assertEqual(1, News.objects.count())
        news = News.objects.all()[0]
        self.assert_correct_removed_message(
            news.title,
            source_package_name, repository
        )

    def test_only_one_version_removed(self):
        """
        Tests the case where a version is removed from the repository, but the
        repository still contains other versions of the same package.
        """
        source_package_name = 'dummy-package'
        versions = ['1.0.0', '1.1.0']
        repository = 'repo'
        for version in versions:
            self.create_source_package(
                source_package_name, version, events=False)
            self.add_source_package_to_repository(
                source_package_name, version, repository, events=False)
        # Remove only one of the versions
        removed_version = versions[0]
        self.remove_source_package_from_repository(
            source_package_name, removed_version, repository)

        self.run_task()

        # No news are generated
        self.assertEqual(0, News.objects.count())

    def test_migrate_and_remove(self):
        """
        Tests the case where a single package version is simultaneously
        added to one repository and removed from another.
        """
        source_package_name = 'dummy-package'
        version = '1.0.0'
        repositories = ['repo1', 'repo2']

        self.create_source_package(
            source_package_name, version, events=False)
        self.add_source_package_to_repository(
            source_package_name, version, repositories[0], events=False)
        # Add the version to one repository
        self.add_source_package_to_repository(
            source_package_name, version, repositories[1])
        # Remove it from the one that already had it
        self.remove_source_package_from_repository(
            source_package_name, version, repositories[0])

        self.run_task()

        # Two news items - removed from one repositories, migrated to another
        self.assertEqual(2, News.objects.count())
        news1, news2 = News.objects.all()
        if repositories[1] in news1.title:
            news1, news2 = news2, news1
        self.assert_correct_removed_message(
            news1.title, source_package_name, repositories[0])
        self.assert_correct_migrated_message(
            news2.title, source_package_name, version, repositories[1])

    def test_multiple_packages_added_same_repo(self):
        """
        Tests the case where multiple new packages are added to the same
        repository.
        """
        names = ['package1', 'package2']
        version = '1.0.0'
        repository = 'repo1'
        for name in names:
            self.create_source_package(name, version)
            self.add_source_package_to_repository(name, version, repository)

        self.run_task()

        self.assertEqual(2, News.objects.count())
        all_news = sorted(News.objects.all(), key=lambda x: x.title)
        for name, news in zip(names, all_news):
            self.assert_correct_accepted_message(
                news.title,
                name, version, repository)
            # The news is linked with the correct package
            self.assertEqual(news.package.name, name)

    def test_multiple_packages_removed_different_repos(self):
        """
        Tests the case where multiple packages are removed from different
        repositories.
        """
        names = ['package1', 'package2']
        version = '1.0.0'
        repositories = ['repo1', 'repo2']
        for name, repository in zip(names, repositories):
            self.create_source_package(name, version, events=False)
            self.add_source_package_to_repository(name, version, repository,
                                                  events=False)
            # Remove the source package from the repository
            self.remove_source_package_from_repository(
                name, version, repository)

        self.run_task()

        self.assertEqual(2, News.objects.count())
        all_news = sorted(News.objects.all(), key=lambda x: x.title)
        for name, news, repository in zip(names, all_news, repositories):
            self.assert_correct_removed_message(
                news.title,
                name, repository)
            # The news is linked with the correct package
            self.assertEqual(news.package.name, name)

    @mock.patch('distro_tracker.auto_news.tracker_tasks.get_resource_content')
    def test_dsc_file_in_news_content(self, mock_get_resource_content):
        """
        Tests that the dsc file is found in the content of a news item created
        when a new package version appears.
        """
        name = 'package'
        version = '1.0.0'
        repository = 'repo'
        self.create_source_package(name, version)
        self.add_source_package_to_repository(name, version, repository)
        expected_content = 'This is fake content'
        mock_get_resource_content.return_value = \
            expected_content.encode('utf-8')

        self.run_task()

        self.assertEqual(1, News.objects.count())
        news = News.objects.all()[0]
        self.assertEqual(news.content, expected_content)

    def test_changelog_entry_in_news_content(self):
        """
        Tests that the news item created for new source package versions
        contains the changelog entry for the version.
        """
        name = 'package'
        version = '1.0.0'
        repository = 'repo'
        src_pkg = self.create_source_package(name, version)
        self.add_source_package_to_repository(name, version, repository)
        changelog_entry = (
            "package (1.0.0) suite; urgency=high\n\n"
            "  * New stable release:\n"
            "    - Feature 1\n"
            "    - Feature 2\n\n"
            " -- Maintainer <email@domain.com>  Mon, 1 July 2013 09:00:00 +0000"
        )
        ExtractedSourceFile.objects.create(
            source_package=src_pkg,
            extracted_file=ContentFile(changelog_entry, name='changelog'),
            name='changelog')

        self.run_task()

        self.assertEqual(News.objects.count(), 1)
        news = News.objects.all()[0]
        self.assertIn(changelog_entry, news.content)
